Julia中的反模式
原文地址: HERE
这篇博文中, 作者主要列举了Julia中四大反模式:[1]
接下来将一一介绍。
未实现异常(NotImplemented Exceptions)
在julia中, 如果用一个未定义某函数方法的类去调用该函数, 会抛出未实现的错误:
通常这可以提供非常有用而且明确的信息, 但有时候也会给用户debug带来问题, 考虑以下例子:
很奇怪吧, 明明定义了GuessingModel
对应的函数方法, 为啥会报这个错误? 细心的读者可能早发现了, 原因是定义GuessingModel
对应的probability_estimate
方法时, 传入的第二个参数是AbstractMatrix
, 但是在调用时, 第二个参数是Vector
类型, 所以实际上调用的最开始适用于AbstractModel
的方法。但是这个报错对解决这个BUG基本上没有帮助(反而还会误导)。
宏编程的滥用
@inbounds
, @simd
, @fastmath
等底层的性能宏, 而是针对能写成函数的情况下, 自己编写的宏。
Julia中宏编程的初衷并不是性能提升, 而是语法转换: @view xs[4:end]
会被转成view(xs, 4:lastindex(xs))
, 这种对end
的转换在function
中是无法实现的。
但是, 现在很多人认为写宏会比写函数更快。(作者认为这很可能是由于90年代C语言的影响造成的, 彼时是通过宏进行inline操作, 从而在编译时可获得更好的性能提升)
2019年的Julia大会上, Steven G. Johnson教授在关于"Adventures in Code Generation"的演讲上的说法就很有参考意义:
更熟悉更易读: 大部分用户更熟悉函数;
高扩展性: 函数更容易进行多重分派, 宏基本上很难扩展;
更容易理解: 函数的基本行为是类似的, 但不同的宏可能内部逻辑大相径庭;
性能非常重要;
字面量解析花费大量时间;
举个栗子:
using BenchmarkTools
# performance using function
compute_poly(x, coeffs) = sum(a * x^(i-1) for (i, a) in enumerate(coeffs))
@btime compute_poly(1, (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17))
# performance using macro
macro compute_poly(x, coeffs_tuple)
# a*x^i
Meta.isexpr(coeffs_tuple, :tuple) || ArgumentError("@compute_poly only accepts a tuple literal as second argument")
coeffs = coeffs_tuple.args
terms = map(enumerate(coeffs)) do (i, a)
a = coeffs[i]
if a isa Number && x isa Number # it is a literal compute at compile time
a * x ^ (i-1)
else
# an expression, so return an expression
esc(:($a * $x ^ $(i-1)))
end
end
if all(x isa Number for x in terms)
# Whole thing can run at compile time
return sum(terms)
else
return Expr(:call, :+, terms...)
end
end
@btime @compute_poly(1, (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17))
宏为啥这么写我暂时还看不太明白, 只需要知道它快就好了:
在我的笔记本上, 函数版用时~70ns
宏版用时1.5ns
;
哈希/字典的误用
作者表示经常看到有人用字典Dict{Symbol}
或Dict{String}
只是用来存储固定的变量(他们想把这些变量打包在一起, 比如配置参数, 模型超参数等)。在Julia 0.7版之前, 这甚至是被提倡的(如果不构建struct
的话)。这实际上会引发至少两个问题:
编写代码的时候, 必须时刻保证所有的操作都不会改变存在Dict中的值;
在大型项目中, 还要无条件相信别人不会错误地改变Dict中的值;
用不可变的数据结构.
O(1)
的常量时间尺度下赋值和取值, 但这个常量时间通常也不会很短, Dict需要计算Hash, Symbol
类型的Hash过程很快, 但String
类型的会慢一些, 如果存储更复杂的结构, hash有可能很慢:
dict = Dict([:a=>1, :b=>2, :c=>3, :d=>4, :e=>5])
@btime $(dict)[:d]; # 5ns
str_dict = Dict(string(k)=> v for (k,v) in dict) # convert all the keys to strings
@btime $(str_dict)["d"]; # 21ns
一个可行的替代方案是OrderedCollection包中的LittleDict, 该结构类似Dict, 但是不会构建hash, 所以比Dict更适用于小集合的存储。
using OrderedCollections
little_dict = LittleDict(dict)
@btime $(little_dict)[:d]; # 8ns
还可以用freeze
方法转成不可变类型:
frozen_little_dict = freeze(LittleDict(dict))
@btime $(frozen_little_dict)[:d]; # 4ns
过度类型标注
NOTE: 这并不是说类型标注不能用于其他场景, 通常它可以在其他场景被很好地应用。但这不改变它被发明的初衷就是用来做派发的事实。
过度类型标注往往来自于以下几个误解:
总之, 由于JIT, julia中真正执行的指令总是针对输入的参数类型优化过的, (这可能也是为啥Julia做静态编译库很困难的主要原因吧)
写详细的文档: Julia的documentation有多强大, 用过的都知道;
写好注释: 众所周知, 写代码最多时候都是在写注释;
:
,{}
,()
,<:
, 跟Perl一样让人眼花。
最后, 作者举了几个例子直观表示类型标注有可能带来的问题:
function my_average(xs::AbstractVector)
len = 0
total = zero(eltype(xs))
for x in xs
len += 1
total += x
end
return total/len
end
# error when input tuple:
my_average((1,2,3))
# error when input type unions:
data = [1, 2, 3, missing, 5, 4] # typeof(data) = Vector{Union{Missing, Int64}}
nmdata = skipmissing(data) # typeof(nmdata) = Base.SkipMissing{Vector{Union{Missing, Int64}}}
my_average(nmdata) # type error
在这个例子中, 指定了my_average
函数适用于AbstractVector
类型, 但是实际上, 有很多可以迭代的类型是不属于AbstractVector
的, 如果用collect(itr)把他们转成AbstractVector
又会增加不必要的内存分配, 降低代码性能。
using BenchmarkTools
function indmin(x::AbstractVector{<:Real})
ind=1
for ii in eachindex(x)
if x[ii] < x[ind]
ind = ii
end
end
return ind
end
indmin(1:10)
# the author's returns 385
# but mine correctly returned 1
data = [1, 2, 3, missing]
indmin(@view(data[1:3]))
# error: @view(data[1:3])返回的数组类型也是Union{Missing, Int64}
apply_inner(func::Function, xss) = [[func(x) for x in xs] for xs in xss]
apply_inner(round, [[0.2, 0.9], [1.2, 1.3, 1.6]]) # works fine
apply_inner(Float32, [[0.2, 0.9], [1.2, 1.3, 1.6]]) # ERROR: Float32 is not a Function
为了拓展适用性, 可以把参数类型改成Base.Callable
, 等同于Union{Type, Function}
。但这也不是万全之策, 有些可执行的对象也不属于这两类, 如DiffEqBase.ODESolution
和Flux.chain
。
总结
最后, 作者推荐一些julia高性能的参考资料:
遵守一个格式规范, 比如作者推荐的BlueStyle
作者的另一篇博文: Continuous Delivery
还得修炼啊!